package com.functionx.utils;

import java.lang.reflect.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ReflectionUtil {

    private static final ReferenceDictionary REFERENCE_DICTIONARY = new ReferenceDictionary();

    public static Object getFieldValue(Object object, String fieldName){
        Object fieldValue = null;
        Field field = REFERENCE_DICTIONARY.field(object.getClass(), fieldName);
        try {
            validateAccess(field);
            fieldValue = field.get(object);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new ObjectAccessException(
                    "Could not get field " + field.getName() + "@" + object.getClass(), e);
        }

        return fieldValue;
    }

    public static Object getFieldValue(Class cls, String fieldName){
        Object fieldValue = null;
        Field field = REFERENCE_DICTIONARY.field(cls, fieldName);
        try {
            validateAccess(field);
            fieldValue = field.get(cls);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new ObjectAccessException(
                    "Could not get field " + field.getName() + "@" + cls, e);
        }

        return fieldValue;
    }

    public static void writeField(Object object, String fieldName, Object value) {
        Field field = REFERENCE_DICTIONARY.field(object.getClass(), fieldName);
        try {
            validateAccess(field);
            field.set(object, value);
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
            throw new ObjectAccessException(
                    "Could not set field " + field.getName() + "@" + object.getClass(), e);
        }
    }

    /*private static void validateFieldAccess(Field field)
            throws IllegalAccessException, NoSuchFieldException {
        if (JVMInfo.is15()) {
            if (!field.isAccessible()) {
                Field modifiersField = Field.class.getDeclaredField("modifiers");
                modifiersField.setAccessible(true);
                modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

                field.setAccessible(true);
            }
        } else {
            throw new ObjectAccessException(
                    "Invalid final field " + field.getDeclaringClass().getName() + "." + field
                            .getName());
        }
    }*/

    private static void validateAccess(AccessibleObject accessibleObject)
            throws IllegalAccessException, NoSuchFieldException {
        if (JVMInfo.is15()) {
            if (!accessibleObject.isAccessible()) {
                if(accessibleObject instanceof Field){
                    Field modifiersField = Field.class.getDeclaredField("modifiers");
                    modifiersField.setAccessible(true);
                    modifiersField.setInt(accessibleObject, ((Field)accessibleObject).getModifiers() & ~Modifier.FINAL);
                } else if(accessibleObject instanceof Method) {
                    Field modifiersField = Method.class.getDeclaredField("modifiers");
                    modifiersField.setAccessible(true);
                    modifiersField.setInt(accessibleObject, ((Method)accessibleObject).getModifiers() & ~Modifier.FINAL);
                }

                accessibleObject.setAccessible(true);
            }
        } else {
            String className = null;
            String referenceName = null;

            if(accessibleObject instanceof Field){
                className = ((Field)accessibleObject).getDeclaringClass().toString();
                referenceName = ((Field)accessibleObject).getName();
            } else if(accessibleObject instanceof Method) {
                className = ((Method)accessibleObject).getDeclaringClass().toString();
                referenceName = ((Method)accessibleObject).getName();
            }
            throw new ObjectAccessException(
                    "Invalid final field " + className + "." + referenceName);
        }
    }

    public static Object invokeMethod(Object object, String methodName, Class<?>[] parameterTypes,
            Object[] parameters) {
        Method method = REFERENCE_DICTIONARY.method(object.getClass(), methodName, parameterTypes);
        try {
            validateAccess(method);
            return method.invoke(object, parameters);
        } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) {
            throw new ObjectAccessException(
                    "Could not invoke " + object.getClass() + "." + methodName);
        }
    }

    private static class ReferenceDictionary {
        private final static Map<Class, Map<String, Field>> fields = new ConcurrentHashMap<>();

        private final static Map<Class, Map<String, Method>> methods = new ConcurrentHashMap<>();

        private Field field(Class<?> cls, String name) {
            Field field = null;
            try {
                Map<String, Field> members = fields.get(cls);
                if(members == null){
                    synchronized (fields) {
                        members = fields.get(cls);
                        if(members == null) {
                            members = new ConcurrentHashMap<>();
                            fields.put(cls, members);
                            field = cls.getDeclaredField(name);
                            members.put(name, field);
                        }
                    }
                } else {
                    field = members.get(name);
                    if (field == null) {
                        field = cls.getDeclaredField(name);
                        members.put(name, field);
                    }
                }
            } catch (NoSuchFieldException e) {
                Class superCls = cls.getSuperclass();
                if (superCls != null) {
                    field = field(superCls, name);

                    Map<String, Field> members = fields.get(cls);
                    members.put(name, field);
                } else
                    throw new ObjectAccessException("Could not access field " + name + "@" + cls,
                            e);
            }
            return field;
        }

        private Method method(Class<?> cls, String methodName, Class<?>[] parameterTypes){
            Method method = null;
            try {
                Map<String, Method> members = methods.get(cls);
                if(members == null){
                    synchronized (methods){
                        members = methods.get(cls);
                        if(members == null){
                            members = new ConcurrentHashMap<>();
                            methods.put(cls, members);
                            method = cls.getDeclaredMethod(methodName, parameterTypes);
                            members.put(methodName + parameterTypes, method);
                        }
                    }
                } else {
                    method = members.get(methodName + parameterTypes);
                    if(method == null){
                        method = cls.getDeclaredMethod(methodName, parameterTypes);
                        members.put(methodName + parameterTypes, method);
                    }
                }
            } catch (NoSuchMethodException e) {
                Class superCls = cls.getSuperclass();
                if (superCls != null) {
                    method = method(superCls, methodName, parameterTypes);

                    Map<String, Method> members = methods.get(cls);
                    members.put(methodName + parameterTypes, method);
                } else
                    throw new ObjectAccessException(
                            "Could not access method " + methodName + parameterTypes + "@" + cls,
                            e);
            }

            return method;
        }
    }
}
